package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/user"
	"strconv"
	"strings"
	"sync"

	"path/filepath"

	"time"
)

//Task Task
type Task struct {
	Sum      int64
	RelSpeed int64
	TmpSpeed int64
}

//TaskItem TaskItem
type TaskItem struct {
	ID       int
	Begin    int64
	Curr     int64
	End      int64
	RelSpeed int64
	TmpSpeed int64
}
type processMsg struct {
	ID      int
	Curr    int64
	Peer    float64
	Speed   float64
	MsgTime time.Time
}

var msgChan = make(chan processMsg, 5)
var task = Task{}
var taskItems = make([]TaskItem, 0, num)
var num int
var help bool
var x bool
var force bool
var url string
var output string
var name string
var beginTime time.Time
var maxIdleTime = 60 * time.Second

func init() {
	flag.BoolVar(&help, "h", false, "帮助")
	flag.BoolVar(&x, "x", false, "Debug")
	flag.BoolVar(&force, "f", false, "强制覆盖下载")
	flag.StringVar(&url, "u", "", "文件下载链接")
	flag.IntVar(&num, "n", 8, "并发下载线程数,最多99")
	flag.StringVar(&output, "o", "", "存储文件路径")
	flag.StringVar(&name, "name", "", "存储文件名称")
}
func main() {
	flag.Parse()
	if help || url == "" {
		flag.Usage()
		return
	}
	// url := "https://www.charlesproxy.com/assets/release/4.5.6/charles-proxy-4.5.6-win64.msi"
	// url := "https://mirror.bit.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz"
	if num > 99 {
		num = 99
	}
	if output == "" {
		u, err := user.Current()
		if nil != err {
			panic(err)
		}
		output = filepath.Join(u.HomeDir, "Downloads")
	}
	if err := checkOutput(url, &output); err != nil {
		fmt.Println(err)
		return
	}
	debug("url", url)
	debug("output", output)
	debug("num", num)
	beginTime = time.Now()
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	debug(resp.Header)
	//判断 Content-Type 是否是受支持的文件
	if err := checkContentType(resp.Header.Get("Content-Type")); err != nil {
		fmt.Println(err)
		return
	}
	cLenStr := resp.Header.Get("Content-Length")
	cLen, err := strconv.Atoi(cLenStr)
	if err != nil {
		fmt.Println("Content-Length error: ", cLenStr, err)
		return
	}
	task.Sum = int64(cLen)
	m := cLen / num
	for i := 0; i < num; i++ {
		end := (i+1)*m - 1
		if i == num-1 {
			end = cLen
		}
		taskItems = append(taskItems, TaskItem{ID: i + 1, Begin: int64(i * m), Curr: int64(i * m), End: int64(end)})
	}
	f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		fmt.Println(err.Error())
		return
	}
	go processSpeed()
	go showProcess()
	wg := &sync.WaitGroup{}
	for i := range taskItems {
		wg.Add(1)
		go download(url, &taskItems[i], f, wg)
	}
	wg.Wait()
	time.Sleep(1e8)
	fmt.Printf("\033[40;32m文件下载完成,耗时: %s, 路径: %s\033[0m", time.Since(beginTime).String(), output)
}

func download(url string, taskItem *TaskItem, dst *os.File, wg *sync.WaitGroup) {
	curr := taskItem.Curr
	defer func() {
		//有异常时, 从新begin开始下载.
		if err := recover(); err != nil {
			taskItem.Curr = curr
			go download(url, taskItem, dst, wg)
		} else {
			wg.Done()
		}
	}()
	client := &http.Client{Transport: &http.Transport{
		Dial: func(netw, addr string) (net.Conn, error) {
			c, err := net.DialTimeout(netw, addr, time.Second*30)
			if err != nil {
				return nil, err
			}
			deadline := time.Now().Add(30 * time.Second)
			c.SetDeadline(deadline)
			return c, nil
		},
	}}
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Add("Range", fmt.Sprintf("bytes=%d-%d", taskItem.Curr, taskItem.End))
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	cacheLen := 10240
	cache := make([]byte, cacheLen)
	for {
		n, err := resp.Body.Read(cache)
		if err != nil && err != io.EOF {
			panic(err)
		}
		if n == 0 {
			break
		}
		_, err = dst.WriteAt(cache[:n], curr)
		if err != nil {
			panic(err)
		}
		taskItem.TmpSpeed += int64(n)
		task.TmpSpeed += int64(n)
		curr = curr + int64(n)
		msgChan <- processMsg{
			ID:      taskItem.ID,
			Curr:    curr - taskItem.Begin,
			Peer:    float64(curr-taskItem.Begin) / float64(taskItem.End-taskItem.Begin) * 100,
			MsgTime: time.Now(),
		}
	}
}

func showProcess() {
	msgMap := make(map[int]processMsg, num)
	printProcessLog(msgMap)
	for {
		select {
		case msg := <-msgChan:
			msgMap[msg.ID] = msg
			fmt.Printf("\033[%dA", len(taskItems)+1)
			printProcessLog(msgMap)
		case <-time.After(1 * time.Second):
			fmt.Printf("\033[%dA", len(taskItems)+1)
			printProcessLog(msgMap)
		}
	}
}

func processSpeed() {
	for {
		select {
		case <-time.Tick(time.Second):
			task.RelSpeed = task.TmpSpeed
			task.TmpSpeed = 0
			for i := range taskItems {
				taskItems[i].RelSpeed = taskItems[i].TmpSpeed
				taskItems[i].TmpSpeed = 0
			}
		}
	}
}

func printProcessLog(msgMap map[int]processMsg) {
	var currSum int64
	for _, v := range msgMap {
		currSum += v.Curr
	}
	since := time.Since(beginTime).String()
	fmt.Printf("%s \033[K\n", fmt.Sprintf("总大小:%s, 已完成:%s, 总进度:%.2f%%, 耗时: %s, 总速度:%s",
		humanSize(task.Sum),
		humanSize(currSum),
		float64(currSum)/float64(task.Sum)*100,
		strings.Split(since, ".")[0]+"s",
		humanSize(task.RelSpeed)))
	for _, v := range taskItems {
		tmp, ok := msgMap[v.ID]
		if !ok {
			fmt.Printf("%s \033[K\n", fmt.Sprintf("任务:%2d, 进度: %6.f%%, 速度: %s", v.ID, 0.0, "0k"))
		} else {
			fmt.Printf("%s \033[K\n", fmt.Sprintf("任务:%2d, 进度: %6.2f%%, 速度: %s", v.ID, tmp.Peer, humanSize(v.RelSpeed)))
		}
	}
}

func humanSize(size int64) string {
	switch {
	case size > 1024*1024*1024:
		return fmt.Sprintf("%.2fG", float64(size)/float64(1024*1024*1024))
	case size > 1024*1024:
		return fmt.Sprintf("%.2fM", float64(size)/float64(1024*1024))
	case size > 1024:
		return fmt.Sprintf("%.fK", float64(size)/float64(1024))
	default:
		return fmt.Sprintf("%dB", size)
	}
}

func checkOutput(url string, output *string) error {
	f, err := os.Stat(*output)
	if os.IsNotExist(err) {
		return os.MkdirAll(*output, os.ModePerm)
	}
	if err == nil && f.IsDir() {
		//追加上文件名
		if name == "" {
			strs := strings.Split(url, "/")
			name = strs[len(strs)-1]
		}
		*output = filepath.Join(*output, name)
		_, err := os.Stat(*output)
		if err == nil && !force {
			return fmt.Errorf("文件[%s]已存在", *output)
		}
		return nil
	}
	return fmt.Errorf("文件[%s]已存在", *output)
}

func checkContentType(contentType string) error {
	switch contentType {
	case "text/html;charset=utf-8":
		return fmt.Errorf("不支持的链接类型")
	}
	return nil
}

func debug(a ...interface{}) {
	if x {
		fmt.Println(a...)
	}
}
